package online.foxnull2.foxmall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import online.foxnull2.common.to.mq.SeckillOrderTo;
import online.foxnull2.common.vo.MemberRespVo;
import online.foxnull2.foxmall.seckill.feign.CouponFeignService;
import online.foxnull2.foxmall.seckill.feign.ProductFeignService;
import online.foxnull2.foxmall.seckill.interceptor.LoginUserInterceptor;
import online.foxnull2.foxmall.seckill.service.SeckillService;
import online.foxnull2.foxmall.seckill.to.SeckillSkuRedisTo;
import online.foxnull2.foxmall.seckill.to.SkuInfoTo;
import online.foxnull2.foxmall.seckill.vo.seckillSession.SeckillSessionVo;
import online.foxnull2.foxmall.seckill.vo.seckillSession.SeckillSkuRelation;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
@Service("SeckillService")
public class SeckillServiceImpl implements SeckillService {

    private static final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private static final String SKU_CACHE_PREFIX = "seckill:skus:";
    private static final String SKU_STOCK_SEMAPHORE_PREFIX = "seckill:stock:";

    private final CouponFeignService couponFeignService;
    private final StringRedisTemplate stringRedisTemplate;
    private final ProductFeignService productFeignService;
    private final RedissonClient redisson;
    private final RabbitTemplate rabbitTemplate;

    @Autowired
    public SeckillServiceImpl(CouponFeignService couponFeignService,
                              StringRedisTemplate stringRedisTemplate,
                              ProductFeignService productFeignService,
                              RedissonClient redisson,
                              RabbitTemplate rabbitTemplate) {
        Assert.notNull(couponFeignService, "CouponFeignService must not be null!");
        this.couponFeignService = couponFeignService;
        Assert.notNull(stringRedisTemplate, "StringRedisTemplate must not be null!");
        this.stringRedisTemplate = stringRedisTemplate;
        Assert.notNull(productFeignService, "ProductFeignService must not be null!");
        this.productFeignService = productFeignService;
        Assert.notNull(redisson, "RedissonClient must not be null!");
        this.redisson = redisson;
        Assert.notNull(rabbitTemplate, "RabbitTemplate must not be null!");
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    public void uploadSeckillSku() {
        List<SeckillSessionVo> seckillSessionVos = couponFeignService.listSeckillSessionWithNextThreeDays();
        if (!ObjectUtils.isEmpty(seckillSessionVos)) {
            saveSessionInfo(seckillSessionVos);
            saveSessionSkuInfo(seckillSessionVos);
            //TODO 提前锁定库存
        }
    }

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        long currentTime = new Date().getTime();
        String pattern = SESSION_CACHE_PREFIX + "*";
        Set<String> keys = stringRedisTemplate.keys(pattern);
        if (ObjectUtils.isEmpty(keys)) {
            return null;
        }
        BoundHashOperations<String, String, String> operations = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        for (String key : keys) {
            String[] timeSection = key.replace(SESSION_CACHE_PREFIX, "").split("_");
            long startTime = Long.parseLong(timeSection[0]);
            long endTime = Long.parseLong(timeSection[1]);
            if (startTime <= currentTime && currentTime <= endTime) {
                List<String> values = stringRedisTemplate.opsForList().range(key, -100, 100);
                if (!ObjectUtils.isEmpty(values)) {
                    List<String> objects = operations.multiGet(values);
                    if (!ObjectUtils.isEmpty(objects)) {
                        List<SeckillSkuRedisTo> seckillSkuRedisTos = objects.stream()
                                .map(o -> JSON.parseObject(o, SeckillSkuRedisTo.class))
                                .collect(Collectors.toList());
                        log.debug("getCurrentSeckillSkus -> {}", seckillSkuRedisTos);
                        return seckillSkuRedisTos;
                    }
                }
                break;
            }
        }
        return null;
    }

    @Override
    public SeckillSkuRedisTo getSeckillInfoBySkuId(Long skuId) {
        BoundHashOperations<String, String, String> operations = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        Set<String> keys = operations.keys();
        if (!ObjectUtils.isEmpty(keys)) {
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(regx, key)) {
                    String text = operations.get(key);
                    SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(text, SeckillSkuRedisTo.class);
                    if (!ObjectUtils.isEmpty(seckillSkuRedisTo)) {
                        long currentTime = new Date().getTime();
                        long startTime = seckillSkuRedisTo.getStartTime();
                        long endTime = seckillSkuRedisTo.getEndTime();
                        if (currentTime < startTime || endTime < currentTime) {
                            seckillSkuRedisTo.setRandomCode(null);
                        }
                    }
                    log.debug("getSeckillInfoBySkuId -> {}", seckillSkuRedisTo);
                    return seckillSkuRedisTo;
                }
            }
        }
        return null;
    }

    @Override
    public String seckill(String seckillId, String code, Integer num) {
        MemberRespVo memberRespVo = LoginUserInterceptor.threadLocal.get();
        Long userId = memberRespVo.getId();
        BoundHashOperations<String, String, String> operations = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        String text = operations.get(seckillId);
        SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(text, SeckillSkuRedisTo.class);
        if (ObjectUtils.isEmpty(seckillSkuRedisTo)) {
            return null;
        }
        long currentTime = new Date().getTime();
        long startTime = seckillSkuRedisTo.getStartTime();
        long endTime = seckillSkuRedisTo.getEndTime();
        if (currentTime < startTime || endTime < currentTime) {
            return null;
        }
        String randomCode = seckillSkuRedisTo.getRandomCode();
        Long promotionSessionId = seckillSkuRedisTo.getPromotionSessionId();
        Long skuId = seckillSkuRedisTo.getSkuId();
        String s = promotionSessionId + "_" + skuId;
        if (!randomCode.equals(code) || !s.equals(seckillId)) {
            return null;
        }
        Integer seckillLimit = seckillSkuRedisTo.getSeckillLimit();
        if (num > seckillLimit) {
            return null;
        }
        long ttl = endTime - currentTime;
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(userId + "_" + seckillId, num.toString(), ttl, TimeUnit.MILLISECONDS);
        if (Boolean.FALSE.equals(aBoolean)) {
            return null;
        }
        String name = SKU_STOCK_SEMAPHORE_PREFIX + randomCode;
        RSemaphore semaphore = redisson.getSemaphore(name);
        boolean b = semaphore.tryAcquire(num);
        if (!b) {
            return null;
        }
        String orderSn = UUID.randomUUID().toString().replace("-", "");
        BigDecimal seckillPrice = seckillSkuRedisTo.getSeckillPrice();
        SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
        seckillOrderTo.setOrderSn(orderSn)
                .setPromotionSessionId(promotionSessionId)
                .setSkuId(skuId)
                .setSeckillPrice(seckillPrice)
                .setNum(num)
                .setMemberId(userId);
        rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);
        log.debug("seckill -> {}", orderSn);
        return orderSn;
    }

    private void saveSessionSkuInfo(List<SeckillSessionVo> seckillSessionVos) {
        BoundHashOperations<String, String, String> operations = stringRedisTemplate.boundHashOps(SKU_CACHE_PREFIX);
        for (SeckillSessionVo seckillSessionVo : seckillSessionVos) {
            long startTime = seckillSessionVo.getStartTime().getTime();
            long endTime = seckillSessionVo.getEndTime().getTime();
            List<SeckillSkuRelation> seckillSkuRelations = seckillSessionVo.getSeckillSkuRelations();
            for (SeckillSkuRelation seckillSkuRelation : seckillSkuRelations) {
                Long promotionSessionId = seckillSkuRelation.getPromotionSessionId();
                Long skuId = seckillSkuRelation.getSkuId();
                String key = promotionSessionId + "_" + skuId;
                String randomCode = UUID.randomUUID().toString().replace("-", "");
                if (Boolean.TRUE.equals(operations.hasKey(key))) {
                    continue;
                }
                SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                seckillSkuRedisTo.setId(seckillSkuRelation.getId())
                        .setPromotionId(seckillSkuRelation.getPromotionId())
                        .setPromotionSessionId(promotionSessionId)
                        .setSkuId(skuId)
                        .setSeckillPrice(seckillSkuRelation.getSeckillPrice())
                        .setSeckillCount(seckillSkuRelation.getSeckillCount())
                        .setSeckillLimit(seckillSkuRelation.getSeckillLimit())
                        .setSeckillSort(seckillSkuRelation.getSeckillSort());
                SkuInfoTo skuInfoTo = productFeignService.infoTo(skuId);
                seckillSkuRedisTo.setSkuInfoTo(skuInfoTo);
                seckillSkuRedisTo.setStartTime(startTime);
                seckillSkuRedisTo.setEndTime(endTime);
                seckillSkuRedisTo.setRandomCode(randomCode);
                String jsonString = JSONObject.toJSONString(seckillSkuRedisTo);
                operations.put(key, jsonString);
                long currentTime = new Date().getTime();
                long ttl = endTime - currentTime;
                stringRedisTemplate.expire(key, ttl, TimeUnit.MILLISECONDS);
                String name = SKU_STOCK_SEMAPHORE_PREFIX + randomCode;
                if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(name))) {
                    continue;
                }
                RSemaphore semaphore = redisson.getSemaphore(name);
                Integer seckillCount = seckillSkuRedisTo.getSeckillCount();
                semaphore.trySetPermits(seckillCount);
                stringRedisTemplate.expire(name, ttl, TimeUnit.MILLISECONDS);
            }
        }
    }

    private void saveSessionInfo(List<SeckillSessionVo> seckillSessionVos) {
        for (SeckillSessionVo seckillSessionVo : seckillSessionVos) {
            long startTime = seckillSessionVo.getStartTime().getTime();
            long endTime = seckillSessionVo.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean bool = stringRedisTemplate.hasKey(key);
            if (Boolean.TRUE.equals(bool)) {
                return;
            }
            List<String> skuIds = seckillSessionVo.getSeckillSkuRelations().stream()
                    .map(seckillSkuRelation -> seckillSkuRelation.getPromotionSessionId() + "_" + seckillSkuRelation.getSkuId())
                    .collect(Collectors.toList());
            stringRedisTemplate.opsForList().leftPushAll(key, skuIds);
            long currentTime = new Date().getTime();
            long ttl = endTime - currentTime;
            stringRedisTemplate.expire(key, ttl, TimeUnit.MILLISECONDS);
        }
    }
}
